﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Engine.Dtd.Schema;
using System.Xml;
using System.Diagnostics;

namespace Engine.Sgml.Parser {
    internal sealed class DoctypeReader : IElementReader {
        private AttributeReader attributeReader;
        private StringBuilder builder;

        public DoctypeReader(AttributeReader attributeReader, StringBuilder builder) {
            Guard.ArgumentNotNull(attributeReader, "attributeReader");
            Guard.ArgumentNotNull(builder, "builder");
            this.attributeReader = attributeReader;
            this.builder = builder;
            }

        public SElement Read(SgmlStreamReader reader, SElement parent, Context context) {
            SElement doctype = parent.Nodes.Push();
            doctype.Clear();
            doctype.Parent = parent;
            doctype.LineNumber = context.LineNumber;
            doctype.LinePosition = context.LinePosition;

            string token = context.NameReader.ScanNameToken(reader);
            if (!token.Equals("DOCTYPE", SObject.NameComparisonType)) {
                context.TraceSource.TraceEvent(TraceEventType.Error, 0, "Unexpected token '{0}' in DOCTYPE element.", token);
                doctype.Name = token;
                doctype.NodeType = SgmlNodeType.Element;
                attributeReader.Read(reader, doctype, context);
                if (reader.CurrentChar != Characters.EndTagMarkup) {
                    reader.SkipToEnd(new[] { Characters.EndTagMarkup, Characters.StartTagMarkup });
                    context.TraceSource.TraceEvent(TraceEventType.Error, 0, "DOCTYPE element starting on {0} was not closed.", doctype.Context());
                    }
                if (reader.CurrentChar == Characters.EndTagMarkup)
                    reader.ReadChar();
                return doctype;
                }

            char ch = reader.SkipWhitespace();
            if (ch == Characters.EndTagMarkup)
                reader.ReadChar();
            else {
                if (ch == Characters.LeftBracket) {
                    ReadInternalSubset(reader, doctype, context);
                    ch = reader.SkipWhitespace();
                    }
                else {
                    // Read doctype name
                    token = context.NameReader.ReadNameToken(reader);
                    ch = reader.SkipWhitespace();
                    if (!string.Equals(token, null) &&
                        !string.Equals(token, "PUBLIC", SObject.NameComparisonType) &&
                        !string.Equals(token, "SYSTEM", SObject.NameComparisonType)) {
                        doctype.Name = token;
                        if (ch == Characters.EndTagMarkup || ch == Characters.LeftBracket)
                            token = string.Empty;
                        else {
                            token = context.NameReader.ScanNameToken(reader);
                            ch = reader.SkipWhitespace();
                            }
                        }
                    else
                        doctype.Name = context.EntityResolver.Schema.Name.ToString();
                    if (string.Equals(token, "PUBLIC", SObject.NameComparisonType) ||
                        string.Equals(token, "SYSTEM", SObject.NameComparisonType)) {
                        if (ch == Characters.QuotationMark || ch == Characters.Apostrophe) {
                            SAttribute attribute = doctype.Attributes.Push();
                            attribute.Reset(token.ToUpperInvariant());
                            attribute.Value = ReadDoctypePublicLiteral(reader, ch);
                            attribute.NodeType = SgmlNodeType.Attribute;
                            attribute.Parent = doctype;
                            ch = reader.SkipWhitespace();
                            }
                        }
                    if (char.IsLetter(ch)) {
                        token = reader.ReadToEnd(new[] { Characters.QuotationMark, Characters.Apostrophe, Characters.LeftBracket, Characters.StartTagMarkup, Characters.EndTagMarkup });
                        context.TraceSource.TraceEvent(TraceEventType.Error, 0, "Unexpected token '{0}{1}' in DOCTYPE element. {2}", ch, token, reader.Context());
                        if (reader.EndOfStream)
                            context.TraceSource.TraceEvent(TraceEventType.Error, 0, "DOCTYPE starting on {0} was not closed.", doctype.Context());
                        ch = reader.CurrentChar;
                        }
                    // Read external ID if exists
                    if (ch == Characters.QuotationMark || ch == Characters.Apostrophe) {
                        SAttribute attribute = doctype.Attributes.Push();
                        attribute.Reset("SYSTEM");
                        attribute.Value = ReadLiteral(reader, ch, context.EntityResolver);
                        attribute.NodeType = SgmlNodeType.Attribute;
                        attribute.Parent = doctype;
                        ch = reader.SkipWhitespace();
                        }
                    }
                if (ch == Characters.LeftBracket) {
                    ReadInternalSubset(reader, doctype, context);
                    ch = reader.SkipWhitespace();
                    }
                if (ch == Characters.EndTagMarkup)
                    reader.ReadChar();
                else {
                    context.TraceSource.TraceEvent(TraceEventType.Error, 0, "Expecting end of DOCTYPE element, but found '{0}'. {1}", ch, reader.Context());
                    reader.SkipToEnd(new[] { Characters.EndTagMarkup, Characters.StartTagMarkup });
                    if (reader.EndOfStream)
                        context.TraceSource.TraceEvent(TraceEventType.Error, 0, "DOCTYPE starting on {0} was not closed.", doctype.Context());
                    if (reader.CurrentChar == Characters.EndTagMarkup)
                        reader.ReadChar();
                    }
                // External DTD if exists didn't loaded.
                }
            // In SGML DOCTYPE SYSTEM is optional, but in XML it is required,
            // therefore if there is no SYSTEM literal then add an empty one.
            if (!doctype.HasAttributes || !doctype.Attributes.Contains("SYSTEM")) {
                SAttribute attribute = doctype.Attributes.Push();
                attribute.Reset("SYSTEM");
                attribute.Value = string.Empty;
                attribute.NodeType = SgmlNodeType.Attribute;
                attribute.Parent = doctype;
                }
            doctype.NodeType = SgmlNodeType.DocumentType;
            builder.Clear();
            return doctype;
            }

        private void ReadInternalSubset(SgmlStreamReader reader, SElement doctype, Context context) {
            int startLine = reader.LineNumber;
            doctype.Value = reader.ReadToEnd(new[] { Characters.RightBracket });
            if (reader.EndOfStream)
                context.TraceSource.TraceEvent(TraceEventType.Error, 0, "Internal subset of DOCTYPE starting on line {0} was not closed.", startLine);
            if (reader.CurrentChar == Characters.RightBracket)
                reader.ReadChar();
            }

        private string ReadDoctypePublicLiteral(SgmlStreamReader reader, char quote) {
            builder.Clear();
            char currentChar = reader.ReadChar();
            while (currentChar != Characters.EndOfFile && currentChar != quote) {
                if (XmlConvert.IsPublicIdChar(currentChar))
                    builder.Append(currentChar);
                else
                    break;
                currentChar = reader.ReadChar();
                }
            // Consume end quote.
            if (currentChar == quote)
                reader.ReadChar();

            string publicLiteral = builder.ToString();
            builder.Clear();
            return publicLiteral;
            }

        public string ReadLiteral(SgmlStreamReader reader, char quote, EntityResolver entityResolver) {
            builder.Clear();
            reader.ReadChar();
            while (reader.CurrentChar != Characters.EndOfFile && reader.CurrentChar != quote) {
                if (reader.CurrentChar == Characters.StartEntityReference) {
                    entityResolver.Resolve(reader, builder);
                    }
                else {
                    builder.Append(reader.CurrentChar);
                    reader.ReadChar();
                    }
                }
            // Consume end quote.
            reader.ReadChar();
            return builder.ToString();
            }
        }
    }
